/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * GenericPropertiesCreator.java
 * Copyright (C) 2005-2012 University of Waikato, Hamilton, New Zealand
 *
 */
package weka.gui;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import weka.core.ClassDiscovery;
import weka.core.ClassDiscovery.StringCompare;
import weka.core.InheritanceUtils;
import weka.core.Utils;
import weka.core.WekaPackageClassLoaderManager;
import weka.core.WekaPackageManager;

/**
 * This class can generate the properties object that is normally loaded from
 * the <code>GenericObjectEditor.props</code> file (= PROPERTY_FILE). It takes
 * the <code>GenericPropertiesCreator.props</code> file as a template to
 * determine all the derived classes by checking the classes in the given
 * packages (a file with the same name in your home directory overrides the the
 * one in the weka/gui directory/package). <br>
 * E.g. if we want to have all the subclasses of the <code>Classifier</code>
 * class then we specify the superclass ("weka.classifiers.Classifier") and the
 * packages where to look for ("weka.classifiers.bayes" etc.):
 * 
 * <pre>
 * 
 *   weka.classifiers.Classifier=\
 *     weka.classifiers.bayes,\
 *     weka.classifiers.functions,\
 *     weka.classifiers.lazy,\
 *     weka.classifiers.meta,\
 *     weka.classifiers.trees,\
 *     weka.classifiers.rules
 * 
 * </pre>
 * 
 * This creates the same list as stored in the
 * <code>GenericObjectEditor.props</code> file, but it will also add additional
 * classes, that are not listed in the static list (e.g. a newly developed
 * Classifier), but still in the classpath. <br>
 * <br>
 * For discovering the subclasses the whole classpath is inspected, which means
 * that you can have several parallel directories with the same package
 * structure (e.g. a release directory and a developer directory with additional
 * classes). <br>
 * <br>
 * The dynamic discovery can be turned off via the <code>UseDyanmic</code>
 * property in the props file (values: true|false).
 * 
 * @see #CREATOR_FILE
 * @see #PROPERTY_FILE
 * @see #USE_DYNAMIC
 * @see GenericObjectEditor
 * @see weka.core.ClassDiscovery
 * @author FracPete (fracpete at waikato dot ac dot nz)
 * @version $Revision$
 */
public class GenericPropertiesCreator {

    /** whether to output some debug information */
    public final static boolean VERBOSE = false;

    /**
     * name of property whether to use the dynamic approach or the old
     * GenericObjectEditor.props file
     */
    public final static String USE_DYNAMIC = "UseDynamic";

    /**
     * The name of the properties file to use as a template. Contains the packages
     * in which to look for derived classes. It has the same structure as the
     * <code>PROPERTY_FILE</code>
     * 
     * @see #PROPERTY_FILE
     */
    protected static String CREATOR_FILE = "weka/gui/GenericPropertiesCreator.props";

    /**
     * The name of the properties file that lists classes/interfaces/superclasses to
     * exclude from being shown in the GUI. See the file for more information.
     */
    protected static String EXCLUDE_FILE = "weka/gui/GenericPropertiesCreator.excludes";

    /** the prefix for an interface exclusion */
    protected static String EXCLUDE_INTERFACE = "I";

    /** the prefix for an (exact) class exclusion */
    protected static String EXCLUDE_CLASS = "C";

    /** the prefix for a superclass exclusion */
    protected static String EXCLUDE_SUPERCLASS = "S";

    /**
     * The name of the properties file for the static GenericObjectEditor (
     * <code>USE_DYNAMIC</code> = <code>false</code>)
     * 
     * @see GenericObjectEditor
     * @see #USE_DYNAMIC
     */
    protected static String PROPERTY_FILE = "weka/gui/GenericObjectEditor.props";

    /** the input file with the packages */
    protected String m_InputFilename;

    /** the output props file for the GenericObjectEditor */
    protected String m_OutputFilename;

    /** the "template" properties file with the layout and the packages */
    protected Properties m_InputProperties;

    /** the output properties file with the filled in classes */
    protected Properties m_OutputProperties;

    /** Globally available properties */
    protected static GenericPropertiesCreator GLOBAL_CREATOR;
    protected static Properties GLOBAL_INPUT_PROPERTIES;
    protected static Properties GLOBAL_OUTPUT_PROPERTIES;

    /**
     * whether an explicit input file was given - if false, the Utils class is used
     * to locate the props-file
     */
    protected boolean m_ExplicitPropsFile;

    /**
     * the hashtable that stores the excludes: key -&gt; Hashtable(prefix -&gt;
     * Vector of classnames)
     */
    protected Hashtable<String, Hashtable<String, Vector<String>>> m_Excludes;

    static {
        try {
            GenericPropertiesCreator creator = new GenericPropertiesCreator();
            GLOBAL_CREATOR = creator;
            if (creator.useDynamic() && !WekaPackageManager.m_initialPackageLoadingInProcess) {
                creator.execute(false, true);
                GLOBAL_INPUT_PROPERTIES = creator.getInputProperties();
                GLOBAL_OUTPUT_PROPERTIES = creator.getOutputProperties();
            } else {
                // Read the static information from the GenericObjectEditor.props
                GLOBAL_OUTPUT_PROPERTIES = Utils.readProperties("weka/gui/GenericObjectEditor.props");
            }
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    /**
     * Get the global output properties
     * 
     * @return the global output properties
     */
    public static Properties getGlobalOutputProperties() {
        return GLOBAL_OUTPUT_PROPERTIES;
    }

    /**
     * Get the global input properties
     * 
     * @return the global input properties
     */
    public static Properties getGlobalInputProperties() {
        return GLOBAL_INPUT_PROPERTIES;
    }

    /**
     * Regenerate the global output properties. Does not load the input properties,
     * instead uses the GLOBAL_INPUT_PROPERTIES
     */
    public static void regenerateGlobalOutputProperties() {
        if (GLOBAL_CREATOR != null) {
            try {
                GLOBAL_CREATOR.execute(false, false);
                GLOBAL_INPUT_PROPERTIES = GLOBAL_CREATOR.getInputProperties();
                GLOBAL_OUTPUT_PROPERTIES = GLOBAL_CREATOR.getOutputProperties();
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    /**
     * initializes the creator, locates the props file with the Utils class.
     * 
     * @throws Exception if loading of CREATOR_FILE fails
     * @see #CREATOR_FILE
     * @see Utils#readProperties(String)
     * @see #loadInputProperties()
     */
    public GenericPropertiesCreator() throws Exception {
        this(CREATOR_FILE);
        m_ExplicitPropsFile = false;
    }

    /**
     * initializes the creator, the given file overrides the props-file search of
     * the Utils class
     * 
     * @param filename the file containing the packages to create a props file from
     * @throws Exception if loading of the file fails
     * @see #CREATOR_FILE
     * @see Utils#readProperties(String)
     * @see #loadInputProperties()
     */
    public GenericPropertiesCreator(String filename) throws Exception {
        super();
        m_InputFilename = filename;
        m_OutputFilename = PROPERTY_FILE;
        m_InputProperties = null;
        m_OutputProperties = null;
        m_ExplicitPropsFile = true;
        m_Excludes = new Hashtable<String, Hashtable<String, Vector<String>>>();
    }

    /**
     * if FALSE, the locating of a props-file of the Utils-class is used, otherwise
     * it's tried to load the specified file
     * 
     * @param value if true the specified file will be loaded not via the
     *              Utils-class
     * @see Utils#readProperties(String)
     * @see #loadInputProperties()
     */
    public void setExplicitPropsFile(boolean value) {
        m_ExplicitPropsFile = value;
    }

    /**
     * returns TRUE, if a file is loaded and not the Utils class used for locating
     * the props file.
     * 
     * @return true if the specified file is used and not the one found by the Utils
     *         class
     * @see Utils#readProperties(String)
     * @see #loadInputProperties()
     */
    public boolean getExplicitPropsFile() {
        return m_ExplicitPropsFile;
    }

    /**
     * returns the name of the output file
     * 
     * @return the name of the output file
     */
    public String getOutputFilename() {
        return m_OutputFilename;
    }

    /**
     * sets the file to output the properties for the GEO to
     * 
     * @param filename the filename for the output
     */
    public void setOutputFilename(String filename) {
        m_OutputFilename = filename;
    }

    /**
     * returns the name of the input file
     * 
     * @return the name of the input file
     */
    public String getInputFilename() {
        return m_InputFilename;
    }

    /**
     * sets the file to get the information about the packages from. automatically
     * sets explicitPropsFile to TRUE.
     * 
     * @param filename the filename for the input
     * @see #setExplicitPropsFile(boolean)
     */
    public void setInputFilename(String filename) {
        m_InputFilename = filename;
        setExplicitPropsFile(true);
    }

    /**
     * returns the input properties object (template containing the packages)
     * 
     * @return the input properties (the template)
     */
    public Properties getInputProperties() {
        return m_InputProperties;
    }

    /**
     * returns the output properties object (structure like the template, but filled
     * with classes instead of packages)
     * 
     * @return the output properties (filled with classes)
     */
    public Properties getOutputProperties() {
        return m_OutputProperties;
    }

    /**
     * loads the property file containing the layout and the packages of the
     * output-property-file. The exlcude property file is also read here.
     * 
     * @see #m_InputProperties
     * @see #m_InputFilename
     */
    protected void loadInputProperties() {
        if (VERBOSE) {
            System.out.println("Loading '" + getInputFilename() + "'...");
        }
        m_InputProperties = new Properties();
        try {
            File f = new File(getInputFilename());
            if (getExplicitPropsFile() && f.exists()) {
                m_InputProperties.load(new FileInputStream(getInputFilename()));
            } else {
                m_InputProperties = Utils.readProperties(getInputFilename());
            }

            // excludes
            m_Excludes.clear();
            Properties p = Utils.readProperties(EXCLUDE_FILE);
            Enumeration<?> enm = p.propertyNames();
            while (enm.hasMoreElements()) {
                String name = enm.nextElement().toString();
                // new Hashtable for key
                Hashtable<String, Vector<String>> t = new Hashtable<String, Vector<String>>();
                m_Excludes.put(name, t);
                t.put(EXCLUDE_INTERFACE, new Vector<String>());
                t.put(EXCLUDE_CLASS, new Vector<String>());
                t.put(EXCLUDE_SUPERCLASS, new Vector<String>());

                // process entries
                StringTokenizer tok = new StringTokenizer(p.getProperty(name), ",");
                while (tok.hasMoreTokens()) {
                    String item = tok.nextToken();
                    // get list
                    Vector<String> list = new Vector<String>();
                    if (item.startsWith(EXCLUDE_INTERFACE + ":")) {
                        list = t.get(EXCLUDE_INTERFACE);
                    } else if (item.startsWith(EXCLUDE_CLASS + ":")) {
                        list = t.get(EXCLUDE_CLASS);
                    } else if (item.startsWith(EXCLUDE_SUPERCLASS)) {
                        list = t.get(EXCLUDE_SUPERCLASS);
                    }
                    // add to list
                    list.add(item.substring(item.indexOf(":") + 1));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * gets whether the dynamic approach should be used or not
     * 
     * @return true if the dynamic approach is to be used
     */
    public boolean useDynamic() {
        if (getInputProperties() == null) {
            loadInputProperties();
        }

        // check our classloader against the system one - if different then
        // return false (as dynamic classloading only works for classes discoverable
        // in the system classpath
        /*
         * if (!ClassLoader.getSystemClassLoader().equals(this.getClass().getClassLoader
         * ())) { if (Boolean.parseBoolean(getInputProperties().getProperty(USE_DYNAMIC,
         * "true")) == true) { System.out.println(
         * "[GenericPropertiesCreator] classloader in use is not the system " +
         * "classloader: using static entries in weka/gui/GenericObjectEditor.props rather "
         * + "than dynamic class discovery."); } return false; }
         */

        return Boolean.parseBoolean(getInputProperties().getProperty(USE_DYNAMIC, "true"));
    }

    /**
     * checks whether the classname is a valid one, i.e., from a public class
     * 
     * @param classname the classname to check
     * @return whether the classname is a valid one
     */
    protected boolean isValidClassname(String classname) {
        return (classname.indexOf("$") == -1);
    }

    /**
     * Checks whether the classname is a valid one for the given key. This is based
     * on the settings in the Exclude file.
     * 
     * @param key       the property key
     * @param classname the classname to check
     * @return whether the classname is a valid one
     * @see #EXCLUDE_FILE
     */
    protected boolean isValidClassname(String key, String classname) {
        boolean result;
        Class<?> cls;
        Class<?> clsCurrent;
        Vector<String> list;
        int i;

        result = true;
        try {
            clsCurrent = WekaPackageClassLoaderManager.forName(classname);
        } catch (Exception ex) {
            clsCurrent = null;
        }

        // are there excludes for this key?
        if (m_Excludes.containsKey(key)) {
            // interface
            if ((clsCurrent != null) && result) {
                list = m_Excludes.get(key).get(EXCLUDE_INTERFACE);
                for (i = 0; i < list.size(); i++) {
                    try {
                        cls = WekaPackageClassLoaderManager.forName(list.get(i).toString());
                        if (InheritanceUtils.hasInterface(cls, clsCurrent)) {
                            result = false;
                            break;
                        }
                    } catch (Exception e) {
                        // we ignore this Exception
                    }
                }
            }

            // superclass
            if ((clsCurrent != null) && result) {
                list = m_Excludes.get(key).get(EXCLUDE_SUPERCLASS);
                for (i = 0; i < list.size(); i++) {
                    try {
                        cls = WekaPackageClassLoaderManager.forName(list.get(i).toString());
                        if (InheritanceUtils.isSubclass(cls, clsCurrent)) {
                            result = false;
                            break;
                        }
                    } catch (Exception e) {
                        // we ignore this Exception
                    }
                }
            }

            // class
            if ((clsCurrent != null) && result) {
                list = m_Excludes.get(key).get(EXCLUDE_CLASS);
                for (i = 0; i < list.size(); i++) {
                    try {
                        cls = WekaPackageClassLoaderManager.forName(list.get(i).toString());
                        if (cls.getName().equals(clsCurrent.getName())) {
                            result = false;
                        }
                    } catch (Exception e) {
                        // we ignore this Exception
                    }
                }
            }
        }

        return result;
    }

    /**
     * fills in all the classes (based on the packages in the input properties file)
     * into the output properties file
     * 
     * @throws Exception if something goes wrong
     * @see #m_OutputProperties
     */
    protected void generateOutputProperties() throws Exception {
        Enumeration<?> keys;
        String key;
        String value;
        String pkg;
        StringTokenizer tok;
        Vector<String> classes;
        HashSet<String> names;
        int i;

        m_OutputProperties = new Properties();
        keys = m_InputProperties.propertyNames();
        while (keys.hasMoreElements()) {
            key = keys.nextElement().toString();
            if (key.equals(USE_DYNAMIC)) {
                continue;
            }
            tok = new StringTokenizer(m_InputProperties.getProperty(key), ",");
            names = new HashSet<String>();

            // get classes for all packages
            while (tok.hasMoreTokens()) {
                pkg = tok.nextToken().trim();

                try {
                    classes = ClassDiscovery.find(WekaPackageClassLoaderManager.forName(key), pkg);
                } catch (Exception e) {
                    System.out.println("Problem with '" + key + "': " + e);
                    classes = new Vector<String>();
                }

                for (i = 0; i < classes.size(); i++) {
                    // skip non-public classes
                    if (!isValidClassname(classes.get(i).toString())) {
                        continue;
                    }
                    // some classes should not be listed for some keys
                    if (!isValidClassname(key, classes.get(i).toString())) {
                        continue;
                    }
                    names.add(classes.get(i));
                }
            }

            // generate list
            value = "";
            classes = new Vector<String>();
            classes.addAll(names);
            Collections.sort(classes, new StringCompare());
            for (i = 0; i < classes.size(); i++) {
                if (!value.equals("")) {
                    value += ",";
                }
                value += classes.get(i).toString();
            }
            if (VERBOSE) {
                System.out.println(pkg + " -> " + value);
            }

            // set value
            m_OutputProperties.setProperty(key, value);
        }
    }

    /**
     * stores the generated output properties file
     * 
     * @throws Exception if the saving fails
     * @see #m_OutputProperties
     * @see #m_OutputFilename
     */
    protected void storeOutputProperties() throws Exception {
        if (VERBOSE) {
            System.out.println("Saving '" + getOutputFilename() + "'...");
        }
        m_OutputProperties.store(new FileOutputStream(getOutputFilename()), " Customises the list of options given by the GenericObjectEditor\n# for various superclasses.");
    }

    /**
     * generates the props-file for the GenericObjectEditor and stores it
     * 
     * @throws Exception if something goes wrong
     * @see #execute(boolean)
     */
    public void execute() throws Exception {
        execute(true, true);
    }

    /**
     * generates the props-file for the GenericObjectEditor
     * 
     * @param store true if the generated props should be stored
     * @throws Exception
     */
    public void execute(boolean store) throws Exception {
        execute(store, true);
    }

    /**
     * generates the props-file for the GenericObjectEditor and stores it only if
     * the the param <code>store</code> is TRUE. If it is FALSE then the generated
     * properties file can be retrieved via the <code>getOutputProperties</code>
     * method.
     * 
     * @param store          if TRUE then the properties file is stored to the
     *                       stored filename
     * @param loadInputProps true if the input properties should be loaded
     * @throws Exception if something goes wrong
     * @see #getOutputFilename()
     * @see #setOutputFilename(String)
     * @see #getOutputProperties()
     */
    public void execute(boolean store, boolean loadInputProps) throws Exception {
        // read properties file
        if (loadInputProps) {
            loadInputProperties();
        }

        // generate the props file
        generateOutputProperties();

        // write properties file
        if (store) {
            storeOutputProperties();
        }
    }

    /**
     * for generating props file:
     * <ul>
     * <li>no parameter: see default constructor</li>
     * <li>1 parameter (i.e., filename): see default constructor +
     * setOutputFilename(String)</li>
     * <li>2 parameters (i.e, filenames): see constructor with String argument +
     * setOutputFilename(String)</li>
     * </ul>
     * 
     * @param args the commandline arguments
     * @throws Exception if something goes wrong
     * @see #GenericPropertiesCreator()
     * @see #GenericPropertiesCreator(String)
     * @see #setOutputFilename(String)
     */
    public static void main(String[] args) throws Exception {
        GenericPropertiesCreator c = null;

        if (args.length == 0) {
            c = new GenericPropertiesCreator();
        } else if (args.length == 1) {
            c = new GenericPropertiesCreator();
            c.setOutputFilename(args[0]);
        } else if (args.length == 2) {
            c = new GenericPropertiesCreator(args[0]);
            c.setOutputFilename(args[1]);
        } else {
            System.out.println("usage: " + GenericPropertiesCreator.class.getName() + " [<input.props>] [<output.props>]");
            System.exit(1);
        }

        c.execute(true);
    }
}
